---
sidebar_position: 2.6
description: Seata是一款开源的分布式事务解决方案
---

:::tip
学习分布式事务之前，需要先搞清楚本地事务，本地事务是分布式事务的基础，在业务中需要注意区分本地事务和分布式事务，滥用分布式事务会影响工程性能。
:::

## Spring 声明式事务 @Transactional 注解
* `@Transactional` 是 Spring 事务管理提供的注解，在一个方法中加上了这个注解，那么这个方法就是有事务的，方法内的操作要么一起提交，要么回滚。
![](./img/@Transactional注解属性.png#pic_center)

### Propagation
事务的传播行为，表示被调用者的事务和调用者的事务之间的关系。<br/>
表示当一个事务传播行为修饰的方法被另一个方法调用时，事务如何进行传播。不同的 propagation 配置会有不同的效果<br/>
1. methodB 是事务修饰的方法
2. methodA 调用了 methodB ，那么事务应该如何传播呢？


:::tip
99% 的情况下都使用 `Propagation.REQUIRED`。
:::
#### 1. 默认传播行为：Propagation.REQUIRED
* 支持当前事务;如果当前没有事务，则新建一个事务。（日常开发中最常使用的配置）
![](./img/Propagation.REQUIRED.png#pic_center)
#### 2. Propagation.REQUIRE_NEW
![](./img/Propagation.REQUIRE_NEW.png#pic_center)
* 新建事务；如果当前存在事务，则把当前事务挂起
* 由于 methodA、methodB 它们的方法使用是两个不同的事务，当 methodB 方法提交后，即使 methodA 方法失败回滚了，
也不会导致 methodB 方法出现回滚；当 methodB 方法失败回滚以后，如果methodA未捕捉到methodB抛出的异常，
导致methodA 继续抛出该异常，methodA方法也会被回滚，如果methodA方法捕获了methodB方法所抛出的异常，则methodA所在的事务还是有可能提交成功的，因为当前的事务已经被挂起了。

#### 3. Propagation.SUPPORTS
* 支持当前事务;否则将以非事务方式执行
![](./img/Propagation.SUPPORT.png#pic_center)
#### 4. Propagation.MANDATORY
![](./img/Propagation.MANDATORY.png#pic_center)
* 支持当前事务；如果没有将抛出 IllegalTransactionStateException 异常
* 会打断 methodA 方法的执行，除非在 methodA 方法中捕获并处理这个异常。
#### 5.Propagation.NOT_SUPPORT
* 不支持当前事务，而是始终以非事务的方式执行
![](./img/Propagation.NOT_SUPPORT.png#pic_center)

#### 6. Propagation.NEVER
* 以非事务的方式执行；如果当前存在事务，则抛出 IllegalTransStateException 异常
![](./img/Propagation.NEVER.png#pic_center)
* 要求调用者方法不存在事务注解，很少使用
#### 7. Propagation.NESTED
* 如果当前存在事务，则对于该传播行为修饰的方法会依然使用当前事务
![](./img/Propagation.NESTED.png#pic_center)
* 基本上不会使用到
### isolation
事务的隔离级别，决定了事务的完整性，默认使用当前数据库默认的隔离级别，通常不会去修改这个属性，
### timeout
事务的过期时间或者超时时间，默认是当前数据库默认的过期时间或者超时时间，通常不需要额外定制，使用数据库的即可。
### readOnly
指定是否为只读事务，默认是 false ，如果在一次执行单条查询语句，则没有必要启用事务支持，数据库默认启用 SQL 执行期间的读一致性。<br/>
如果一次执行多条查询语句，比如统计查询，报表查询;在这种场景下，多条查询的 SQL 必须保证整体的一致性，否则在前一条SQL查询之后，后一条 SQL 查询之前，数据被其它用户改变，这次整体的统计查询将会出现读
数据不一致的状态，此时应该启用只读事务的支持。<br/>
对于只读查询，可以指定事务类型为 readOnly， 由于只读事务不存在数据的修改，所以数据库将为只读事务添加一些优化手段。
### rollbackFor、noRollbackFor
rollbackFor 指定那些异常会导致事务回滚。<br/>
noRollbackFor 指定那些异常不会导致事务回滚。<br/>
`@Transactional` 注解默认只针对 RuntimeException 、Error 及其子类进行回滚，如果希望对检查型的异常进行回滚就需要对 rollbackFor 属性进行配置。<br/>
noRollbackFor 属性正好相反，用于指定那些异常不发生回滚。


### @Transactional 注解最常见的应用
* 可以标注在类、方法和接口(但接口上会失效，所以不能这样用)上；且方法上的注解会覆盖类上的注解
* 标注在方法上，标识开启事务功能，正常则提交、异常则回滚
* 自行指定rollbackFor属性，让Checked Exception 也能够实现回滚
* 让 TestCase 也能够实现回滚（只需要在测试方法上面加上注解）。如果已经有了 `@Transactional` 注解在类上面。但是，还是想要不回滚某个单元测试，加上 @Rollback(value=false)

### @Transactional注解失效的场景
#### 场景一: 把注解标注在非 public 修饰的方法上
把注解标注在非 public 修饰的方法上，SpringAOP 代理时，最终会调用一个方法（`TransactionalRepositoryProxyPostProcessor#AbstractFallbackTransactionAttributeSource#computeTransactionAttribute(Method method, Class<?> targetClass)`）去计算注解的属性是否符合要求，这个方法的第一行就是去检查方法是不是 public 的，如果不是则不能获取 @Transactional 注解的配置信息。虽然事务没有生效，但是编译运行代码不会有任何错误。
    ```java
    private TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
if (this.allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
}
```
#### 场景二：propagation(传播行为)属性配置错误
* Propagation.SUPPORTS： 如果调用方没有事务，那么会以非事务的方式运行。
* Propagation.NOT_SUPPORTS : 会挂起当前事务，那么会以非事务的方式运行
* Propagation.NEVER: 如果有事务，会抛出异常
#### 场景三：rollbackFor属性设置错误
rollbackFor 能够指定触发事务回滚的异常类型，Spring默认抛出了非检查型的异常或者是Error 才会回滚事务。如果在事务中抛出了其它类型的异常，但却希望Spring能够回滚事务就需要自己指定 rollbackFor属性
#### 场景四：在同一个类中方法调用，导致事务失效
比如有一个类有两个方法 methodA 和 methodB，methodA 去调用类里面的 methodB ,不管 methodB 的访问修饰符是什么，如果 methodA 没有声明注解事务，而 methodB 有 ，则 外部调用 methodA 以后，
methodB 的事务是不会起作用的。

**只有事务方法被当前类以外的代码调用的时候，才会由 Spring 生成的代理对象去管理**

#### 场景五：自己主动去catch，代表 【没有异常】，导致事务失效
#### 场景六：数据库引擎本身就不支持事务（例如 MyISAM），当然也不会生效
事务从根本上就失效了，因为 Spring 也要依赖基础设施。


## 分布式事务理论
### 什么是分布式事务
* 【**跨进程的事务**】 分布式事务是来源于微服务的（或分布式），服务之间存在着调用，且整个调用链路上存在着多处（分布在不同的微服务上）写数据表的行为，就需要分布式事务保证这些操作要么全部成功，要么全部失败
* 判断分布式事务的依据是，是否跨进程，是否有多个本地事务。

![](./img/分布式事务使用场景.png#pic_center)

* 比如创建订单，创建订单的操作有三个依赖调用 （扣减余额、扣减库存、物流订单），这三个依赖调用分别是另外的三个微服务完成的。在这三个微服务中的操作都会发生数据表记录的变更以及本地事务的发生，所以创建订单的一系列操作就是一个分布式事务。
#### 分布式事务追求的一致性条件（根据业务特性）

* 强一致性：任何一次读都能读到某个数据的最近一次写的数据（要求最高，也是最常见的一致性要求）
* 弱一致性：数据更新后，如果能容忍后续的访问只能访问到部分或者全部都访问不到，则是弱一致性（绝大部分业务场景都不允许，最常见的常见是推送，并不需要推送一定能够成功）
* 最终一致性：不保证在任意时刻任意节点的数据都是完整的（状态一致），但是，随着时间的推移（会有个度量），数据总会达到一致的状态。这类场景用在异步操作里面。特别是消息驱动的场景下，应用非常广泛。

## 分布式事务解决方案概览
### 两阶段提交
* 两阶段指的是分两步提交；存在一个中央协调器负责协调各个**分支事务**

![](./img/分布式事务解决方案-两阶段提交.png#pic_center)

* 中央协调器在第一阶段的时候去询问各个资源管理器是否都就绪了，如果收到各个资源管理起的响应都是就绪，进入第二阶段
* 中央协调器按顺序提交事务，如果其中的任意一个资源的响应是 no，则回滚事务。

**两阶段提交属于强一致性策略的实现**

:::tip
1. 这种方式存在缺陷，并且存在单点故障的风险。
2. 当参与事务者存在占用公共资源的情况，其它事务参与者只能够阻塞等待资源的释放，处于阻塞状态。
:::
### 本地消息表
![](./img/分布式事务解决方案-本地消息表.png#pic_center)

* 该方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行
* 消息日志可以存储在数据库

**本地消息表实现的是最终一致性的策略**

## 分布式事务解决方案 Seata
* Seata 是一款开源的分布式事务解决方案，致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
* 官网：http://seata.io/zh-cn
### Seata 的过程模型
![](./img/Seata的过程模型.png#pic_center)

Seata 中有三个重要角色：TC、TM、RM
* TM： 事务的发起者（执行分布式事务的地方），用于通知 TC，全局事务的开始、提交、回滚
* RM： 事务资源，每一个RM都会作为一个分支事务注册在TC上
* TC：事务协调者，也就是中央协调器；用于接受事务的注册、提交、回滚

### Seata 的事务模式 AT
* Seata 定义了全局事务的框架；全局事务定义为若干分之事务的整体协调

1. TM 向 TC 发起请求、提交、回滚全局事务（TM向TC申请全局事务，TC会创建全局事务并返回唯一的 XID，XID 是一个事务的唯一标识，数据表的主键ID）
2. TM 把代表全局事务的 XID 绑定到分支事务上（每一个分支事务都会携带有 TC 颁发的XID，带有同一个 XID 的多个本地事务共同构成一个分布式事务）
3. RM 向 TC 注册，把分支事务关联到 XID 上（必须的，需要让TC知道当前的分布式事务有多少分支事务）
4. RM 把分支事务的执行结果上报给 TC （各个分支事务不论是成功还是失败都需要把各自的结果信息上报给 TC）
5. TC 发送分支提交或分支回滚命令给 RM （TC 综合各个分支事务的结果，去下发提交或者回滚，以此完成一次分布式事务）

:::tip
AT 模式是业界使用最广泛的模式
:::

### Seata AT 模式的整体机制
* AT 采用两阶段提交的方式，对业务代码完全无侵入性，使用简单，改造成本低；只需要关注自己的业务SQL，Seata 会通过分析业务SQL ，反向生成回滚数据。

**AT模式思想核心：成功的时候删除回滚数据，失败的时候则利用回滚数据实现回滚**

![](./img/Seata AT模式整体机制.png#pic_center)

#### 一阶段: 业务数据和回滚日志记录在一个本地事务中提交，释放本地锁和连接资源。
#### 二阶段： 提交异步化，非常快速地完成；回滚通过一阶段的回滚日志进行反向补偿。

### Seata AT 模式工作示例
两个全局事务 tx1 和 tx2 ，分别对 a 表的 m 字段进行更新操作，m的初始值是1000
1. 一阶段本地事务提交前，需要确保先拿到全局锁
2. 拿不到全局锁，不能提交本地事务
3. 拿全局锁的尝试被限制在一定范围内，超过范围将放弃，并回滚本地事务，释放本地锁

4. 如果 tx1 的二阶段全局回滚，则 tx1 需要重新获取该数据的本地锁，进行反向补偿的更新操作，实现分支的回滚。

## Seate 单机搭建
* Seata 下载地址：https://github.com/seata/seata/releases
* Seata 目录结构
```shell
[root@iZ2zeausm2jefqqtmpikzqZ seata]# ll
total 56
drwxr-xr-x 2 root root   4096 Aug 18 17:02 bin
drwxr-xr-x 3 root root   4096 Aug 18 17:02 conf
-rw-r--r-- 1  502 games   896 Jul  6 11:40 Dockerfile
drwxr-xr-x 3 root root   4096 Aug 18 17:02 ext
drwxr-xr-x 3  502 games 16384 Jul 11 11:13 lib
-rw-r--r-- 1  502 games 12079 Jul  6 11:40 LICENSE
drwxr-xr-x 2  502 games  4096 Jun 28 16:19 logs
drwxr-xr-x 5 root root   4096 Aug 18 17:02 script
drwxr-xr-x 2 root root   4096 Aug 18 17:02 target
```
* 启动命令： `nohup sh bin/seata-server.sh -h 127.0.0.1 -p 8091 -m file &`
  * -h: 指定注册中心的IP，不指定时获取当前 IP，外部访问部署在云环境中和容器中的server建议指定
  * -p: 端口号;默认端口号是 8091
  * -m：事务日志存储方式，支持 file、db、redis、默认为file

## SeataAT模式的应用
### Client（微服务使用）Seata（AT模式）的步骤
* pom.xml中引入依赖：`spring-cloud-starter-alibaba-seata`,`HikariCP`
```xml
```
* 创建 `undo_log`表（如果业务使用了多个数据库，每一个数据库都要有这张表,这张表就是存储事务日志的数据表，记录每一个分支事务的回滚日志）
* 配置事务分组，seata 的资源逻辑，类似于服务实例，需要在 file.conf 文件中配置
* 配置 Seata 数据源配置（要使用 Seata ，一定要给工程里的数据源做代理配置，通用代理）
  ```java
  import com.zaxxer.hikari.HikariDataSource;
  import io.seata.rm.datasource.DataSourceProxy;
  import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.context.annotation.Primary;

  import javax.sql.DataSource;

  /**
   * <h1>Seata 所需要的数据库代理配置类</h1>
  */
  @Configuration
  public class DataSourceProxyAutoConfiguration {
  private final DataSourceProperties dataSourceProperties;

      public DataSourceProxyAutoConfiguration(DataSourceProperties dataSourceProperties) {
          this.dataSourceProperties = dataSourceProperties;
      }

      /**
       * <h2>配置数据源代理，用于 Seata 全局事务回滚</h2>
       * DataSourceProxy 代理的是业务数据库的数据源，以此来处理业务数据 ORM 过程
       */
      @Primary
      @Bean("dataSource")
      public DataSource dataSource() {
          HikariDataSource dataSource = new HikariDataSource();
          dataSource.setJdbcUrl(dataSourceProperties.getUrl());
          dataSource.setUsername(dataSourceProperties.getUsername());
          dataSource.setPassword(dataSourceProperties.getPassword());
          dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());

          return new DataSourceProxy(dataSource);
      }

  }

    ```
* 加载拦截器 SeataHandlerInterceptor ，实现微服务之间 xid 的传递
* 将 @GlobalTransactional 注解标注在需要分布式事务的方法上。

### SeataAT模式的工作流程
* AT 模式的工作流程分为两个阶段；一阶段执行业务SQL，二阶段提交或回滚

![](./img/SeataAT模式工作原理.png)

#### 一阶段分支事务的实现原理
#### 二阶段 Commit 流程的实现原理
![](./img/二阶段Commit流程的实现原理.png)


## 分布式事务及解决方案 Seata 总结
![](./img/单体事务和分布式事务.png)
### 微服务应用出现数据不一致的问题
最常见的三个数据不一致的原因
![](./img/微服务应用出现数据不一致的问题.png)

* 一个服务链路调用中，如果调用的过程出现了业务异常，产生异常的应用独立回滚，其它应用已经持久化到数据库
* 由于网络不稳定，导致链路中断，部分应用业务执行完成，部分应用业务未必执行完成
* 服务宕机，无法被正常调用，也会出现数据不一致的情况

:::tip
正是由于没有分布式事务的监管，导致多个本地事务的协同出现了问题，引起了数据不一致，最后导致系统状态出错。所以才需要引入分布式事务。
:::
### Seata 的过程模型
![](./img/Seata是什么.png)
Seata 中的三个重要角色：TC、TM、RM

* TM：事务的发起者，用于通知 TC，全局事务的开始、提交、回滚
* RM：事务资源，每一个 RM 都会作为一个分支事务注册在 TC 上
* TC: 事务协调者，也就是中央协调器；用于接受事务的注册、提交、回滚

### Seata AT 模式的整体机制
* AT 对业务代码完全无侵入性，使用非常简单，改造成本低；只需要关注自己的业务SQL，Seata 会通过分析业务 SQL ，反向生成回滚数据
![](./img/SeataAT模式整体机制.png)

